package mahjong;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

/**
 * Mahjong output analyzer
 * 
 * @author 					<a href=mailto:jaeyounb@usc.edu>Jae young Bang</a>
 * @version					2014.05
 */
public class MahjongOutputAnalyzer {
	
	///////////////////////////////////////////////////////////
	//	Member variables
	///////////////////////////////////////////////////////////
	
	/**
	 * Path to the target directory that contains the .out files
	 */
	protected	Path					targetDir;
	
	/**
	 * Path to the result file
	 */
	protected	Path					resultFile;
	
	/**
	 * List of execution times
	 */
	protected	List<BigInteger> 		execTimes			= new ArrayList<>();
	
	/**
	 * List of mean execution times
	 */
	protected	List<BigInteger>		meanExecTimes		= new ArrayList<>();
	
	/**
	 * Total execution time
	 */
	protected 	BigInteger				totalExecTime 		= new BigInteger("0");
	
	/**
	 * Total number of computations
	 */
	protected 	int						totalComputations	= 0; 
	
	
	/**
	 * Line separator character member
	 */
	protected 	final	String			endl 				= System.lineSeparator();
	
	/**
	 * CharSet member
	 */
	protected 	final	Charset 		charset 			= Charset.forName("UTF-8");

	
	///////////////////////////////////////////////
	//Constructors
	///////////////////////////////////////////////
	
	/**
	 * The default constructor
	 * 
	 * @param targetDir		Target directory that contains .out files
	 * @param resultFile	Path to the result file
	 */
	public MahjongOutputAnalyzer (Path targetDir, Path resultFile) {
		this.targetDir	= targetDir;
		this.resultFile	= resultFile;
	}
	
	/**
	 * Analyze!
	 * 
	 * @throws Exception	File i/o or parsing error
	 */
	public void analyze () throws Exception {
		// Finds .out files in the target dir
		List<Path> 		outFiles 	= findFiles(targetDir);
		
		// The results
		List<String>	results		= new ArrayList<>();
		results.add(new String("com_id, filename, begin_time, end_time, exec_time, mean_exec_time, mean_exec_time_fluc, abs_mean_exec_time_fluc"));
		
		// Lastest mean execution time
		BigInteger		lastMean	= new BigInteger("0");
		
		// Iterates through the list of output files
		for(Path file : outFiles) {
			
			try {
				// Reads the computation begin/end time and computes the "diff" between them 
				Map<String, BigInteger>	times = analyzeFile(file);
				
				// Adds the execution time to the list of execution times
				execTimes.add(times.get("diff"));
				
				// Adds the execution time to the total execution time
				totalExecTime 		= totalExecTime.add(times.get("diff"));
				
				// Increases the total computation number
				totalComputations++;
				
				// Computes the current mean execution time and stores it
				BigInteger newMean 	= computeMean();
				meanExecTimes.add(newMean);
				
				// Computes the difference between the latest mean and the new mean
				BigInteger meanDiff	= newMean.subtract(lastMean);
				lastMean 			= newMean;
				
				results.add(new String(		totalComputations 	+ ", " + 
											file.getFileName() 	+ ", " + 
											times.get("begin")	+ ", " +
											times.get("end")	+ ", " +
											times.get("diff")	+ ", " +
											newMean				+ ", " +
											meanDiff 			+ ", " +
											meanDiff.abs()		+ ", "));
			} catch (Exception e) {
				printError(e.getMessage());
			}
		}
		
		// Prints the result
		writeResult(results);
	}
	
	
	///////////////////////////////////////////////
	//Member Methods
	///////////////////////////////////////////////
	
	/**
	 * Finds and returns the paths of all .out files in the target directory
	 * 
	 * @param targetDir		The target directory
	 * @return				List of .out file paths
	 */
	protected List<Path> findFiles (Path targetDir) {
		
		// List of output files in the target dir
		List<Path> outFiles = new ArrayList<>();
		
		// Iterates through the target dir to find .out files
		try (DirectoryStream<Path> ds = Files.newDirectoryStream(targetDir)) {
			
			for (Iterator<Path> it = ds.iterator(); it.hasNext(); ) {
				
				// Gets a file path
				Path file 		= (Path) it.next();
				
				// If the extension is not .out, skips the file
				if(!getExtension(file).toLowerCase().equals("out")) {
					continue;
				}
				
				// Adds to the list of output files
				outFiles.add(file);
			}
		} catch (IOException ioe) {
			printError("Error while iterating through the .out files: " + ioe);
		}
		
		return outFiles;
	}
	
	/**
	 * Reads an output file and reads the computation begin/end times
	 * 
	 * @param outFilePath	Target output file
	 * @return				Mapping with two keys "begin", "end", "diff" to the corresponding times
	 * @throws Exception	Error while reading/parsing the output file
	 */
	protected Map<String, BigInteger> analyzeFile (Path outFilePath) throws Exception {

		// The map that contains "begin", "end", "diff" times
		Map<String, BigInteger> times = new TreeMap<>();
		
		// Opens the file, reads in the numbers, and analyzes them
		try (BufferedReader br = Files.newBufferedReader(outFilePath, charset)) {
			
			String 		line 		= null;		// A line in the .out file
			BigInteger	timeBegin	= null;		// Computation begin time
			BigInteger 	timeEnd		= null;		// Computation end time
			
			// Reads the first line
			if((line = br.readLine()) != null) {
				// Tokenize the first line
				String[] tokens = line.split(" ");
				
				// Takes the last token and turns it into a BigInteger
				timeBegin = new BigInteger(tokens[tokens.length-1]);
				
				// Assigns the begin time to the map
				times.put("begin", timeBegin);
			} else {
				throw new Exception ("File " + outFilePath.getFileName() + " is empty");
			}
			
			// reads the second line
			if((line = br.readLine()) != null) {
				// Tokenize the second line
				String[] tokens = line.split(" ");
				
				// Takes the last token and turns it into a BigInteger
				timeEnd = new BigInteger(tokens[tokens.length-1]);
				
				// Assigns the begin time to the map
				times.put("end", timeEnd);
			} else {
				throw new Exception ("File " + outFilePath.getFileName() + " does not have the second line");
			}
			
			// computes the "diff" execution time
			times.put("diff", timeEnd.subtract(timeBegin));
			
		} catch (IOException ioe_br) {
			throw new Exception ("Error while reading " + outFilePath.getFileName() + " : " + ioe_br);
		} catch (NumberFormatException nfe) {
			throw new Exception ("Value parsing exception while reading " + outFilePath.getFileName() + ": " + nfe);
		} catch (Exception exc) {
			throw new Exception ("Unexpected exception while reading " + outFilePath.getFileName() + " : " + exc);
		}
		
		return times;
	}
	
	/**
	 * Gets the extension of a file
	 * 
	 * @param file			Path to the file
	 * @return				Extension
	 */
	protected String getExtension (Path file) {
		String filename	= file.getFileName().toString();
		String[] tokens = filename.split("\\.");
		
		return tokens[tokens.length - 1];
	}
	
	/**
	 * Computes the current mean execution time
	 * 
	 * @return				Current mean execution time
	 */
	protected BigInteger computeMean() {
		BigInteger mean = totalExecTime.divide(BigInteger.valueOf(totalComputations));
		return mean;
	}
	
	/**
	 * Prints the results 
	 * 
	 * @param results		Results to print
	 * @throws Exception	File i/o error
	 */
	protected void writeResult(List<String> results) throws Exception {
		try (OutputStream out = Files.newOutputStream(resultFile)) {
			for(String line : results) {
				line += endl;
				out.write(line.getBytes());
			}
		} 
	}
	
	
	///////////////////////////////////////////////
	// The main() method 
	///////////////////////////////////////////////

	public static void main(String[] args) {
		
		printMsg("Mahjong Output Analyzer");
		
		// Checks for the number of arguments
		if(args.length != 2) {
			printUsage();
			return;
		}
		
		// Checks for the target directory
		if(Files.notExists(Paths.get(args[0]), LinkOption.NOFOLLOW_LINKS)) {
			printError("Target directory does not exist.");
			printUsage();
		}
		
		printMsg("Beginning analysis ...");
		
		Path targetDir 	= Paths.get(args[0]);
		Path resultFile	= Paths.get(args[1]);
		
		// Initializes
		MahjongOutputAnalyzer moa = new MahjongOutputAnalyzer(targetDir, resultFile);
		
		// Analyzes
		try {
			moa.analyze();
		} catch (Exception e) {
			printError(e.getMessage());
		}
		
		// Done!
		printMsg("Analysis is complete. The result has been stored in: " + args[1]);
	}
	
	/**
	 * Print usage to screen
	 * 
	 * @param msg			Error message
	 */
	public static void printUsage() {
		System.out.println("Usage: java -jar MOA.jar [target directory] [result filename]");
	}
	
	/**
	 * Print error messages to screen
	 * 
	 * @param msg			Error message
	 */
	public static void printError(String msg) {
		System.out.println("[Error]: " + msg);
	}
	
	/**
	 * Print messages to screen
	 * 
	 * @param msg			Error message
	 */
	public static void printMsg(String msg) {
		System.out.println("[MOA]: " + msg);
	}
}
